﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using LayeredArchitectureSample.DomainModel;
using LayeredArchitectureSample.Infrastructure;

namespace LayeredArchitectureSample.DataAccess
{
    // TODO: optimize route search by using hashes for sets of locations :)
    /// <summary>
    /// </summary>
    /// <remarks> Use TransactionScope to get transactions across several repository operations</remarks>
    public class EntityFrameworkRouteRepository : IRouteRepository
    {
        private readonly IDatabaseContextFactory _dataContextFactory;

        internal EntityFrameworkRouteRepository(IDatabaseContextFactory dataContextFactory)
        {
            if (dataContextFactory == null) throw new ArgumentNullException("dataContextFactory");

            _dataContextFactory = dataContextFactory;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <exception cref="CannotRemoveEntityException"></exception>
        public void RemoveRoute(long id)
        {
            using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
            {
                try
                {
                    Route routeWithGivenId = GetRouteWithGivenIdOrNull(dataContext, id);

                    if (routeWithGivenId == null)
                    {
                        throw new EntityNotFoundException(Resources.CannotRemoveRouteHaventFoundIt.FormatWith(id));
                    }

                    foreach (RouteStep routeStep in routeWithGivenId.RouteSteps.ToArray())
                    {
                        dataContext.DeleteObject(routeStep);
                    }

                    dataContext.DeleteObject(routeWithGivenId);

                    dataContext.SaveChanges();
                }
                catch (UpdateException exception)
                {
                    SqlException innerExceptionAsSqlException = exception.InnerException as SqlException;

                    if (innerExceptionAsSqlException != null &&
                        innerExceptionAsSqlException.Number == Constants.Database.ReferencesToObjectDetectedErrorCode)
                    {
                        throw new CannotRemoveEntityException(Resources.FailedToRemoveRouteReferencedByOtherObject.FormatWith(id), exception);
                    }

                    throw new CannotRemoveEntityException(Resources.FailedToRemoveRoute.FormatWith(id), exception);
                }
                catch (InvalidOperationException)
                {
                    throw new CannotRemoveEntityException(
                        Resources.FailedToRemoveRouteMoreThanOneWithGivenId.FormatWith(id));
                }
                catch (EntityException exception)
                {
                    throw new CannotRemoveEntityException(Resources.FailedToRemoveRoute.FormatWith(id), exception);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="EntityException"></exception>
        private static Route GetRouteWithGivenIdOrNull(LayeredSampleDataModel context, long id)
        {
            return (from Route route in context.Routes
                    where route.Id == id
                    select route).SingleOrDefault();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="CannotAddEntityException"></exception>
        public DomainModel.Route AddNewRoute(IEnumerable<DomainModel.Location> locations)
        {
            // Validation
            var newRouteToBe = new DomainModel.Route(1, locations);

            using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
            {
                MakeSureNoRouteExistsWithSameWaypoints(locations, dataContext);

                try
                {
                    var newRoute = new Route() {};

                    var newRouteSteps = new List<RouteStep>();
                    int routeStepNumber = 0;
                    
                    foreach (var location in locations)
                    {
                        newRouteSteps.Add(new RouteStep()
                                                {
                                                    LocationId = location.Id,
                                                    StepNumber = routeStepNumber,
                                                    Route = newRoute
                                                });

                        routeStepNumber++;
                    }

                    // Note: EF figures out dependencies on its own
                    dataContext.Routes.AddObject(newRoute);

                    dataContext.SaveChanges();

                    return new DomainModel.Route(newRoute.Id, locations);
                    
                }
                catch (EntityException exception)
                {
                    throw new CannotAddEntityException(Resources.FailedToCreateRoute, exception);
                }
            }
        }

        /// <exception cref="CannotAddEntityException"></exception>
        private void MakeSureNoRouteExistsWithSameWaypoints(IEnumerable<DomainModel.Location> locations, LayeredSampleDataModel dataContext)
        {
            try
            {
                GetRouteByWaypoints(locations, dataContext);

                throw new CannotAddEntityException(Resources.RouteWithSameLocationsInThatOrderAlreadyExists);
            }
            catch (EntityException exception)
            {
                throw new CannotAddEntityException(Resources.FailedToAddNewRoute, exception);
            }
            catch (EntityNotFoundException)
            {
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="waypoints"></param>
        /// <returns></returns>
        /// <exception cref="EntityException"></exception>
        /// <exception cref="EntityNotFoundException"></exception>
        private DomainModel.Route GetRouteByWaypoints(IEnumerable<DomainModel.Location> waypoints, LayeredSampleDataModel dataContext)
        {
            // Note: can optimize drastically

            var domainRoute = new DomainModel.Route(1, waypoints);

            foreach (IGrouping<Route, RouteStep> routeToSteps in dataContext.RouteSteps.Include("Location").GroupBy(step => step.Route))
            {
                IEnumerable<DomainModel.Location> 
                    locations = 
                        routeToSteps
                        .OrderBy(step => step.StepNumber)
                        .Select(step => new DomainModel.Location(step.LocationId, step.Location.Name));

                if (domainRoute.DoWaypointsMatch(locations))
                {
                    return RouteToDomain(routeToSteps.Key);
                }
            }

            throw new EntityNotFoundException(Resources.FoundNoRouteWithSpecifiedLocations);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="EntityRetrievalException"></exception>
        public IEnumerable<DomainModel.Route> GetAll()
        {
            using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
            {
                try
                {
                    var routes = new List<DomainModel.Route>();

                    // Eager loading related data
                    foreach (Route route in dataContext.Routes.Include("RouteSteps.Location").Where(r => r.Id != UnspecifiedRoute.Id))
                    {
                        routes.Add(RouteToDomain(route));
                    }

                    return routes;
                }
                catch (EntityException exception)
                {
                    throw new EntityRetrievalException(Resources.FailedToRetrieveRoutes, exception);
                }
            }
        }

        private DomainModel.Route RouteToDomain(Route route)
        {
            return new DomainModel.Route(
                route.Id,
                route.RouteSteps.OrderBy(step => step.StepNumber)
                    .Select(step => new DomainModel.Location(step.Id, step.Location.Name)));
        }
    }
}